/*
 * 
 *  This file is part of Sourceprojects.org Lycia Copyright 2010 noctarius
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * 
 */
package org.sourceprojects.lycia.internal.validation;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.TypeVariable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.sourceprojects.lycia.ConstraintValidator;
import org.sourceprojects.lycia.LyciaParser;
import org.sourceprojects.lycia.annotations.AssertFalse;
import org.sourceprojects.lycia.annotations.AssertTrue;
import org.sourceprojects.lycia.annotations.Constraint;
import org.sourceprojects.lycia.annotations.DecimalMax;
import org.sourceprojects.lycia.annotations.DecimalMin;
import org.sourceprojects.lycia.annotations.Digits;
import org.sourceprojects.lycia.annotations.Empty;
import org.sourceprojects.lycia.annotations.Future;
import org.sourceprojects.lycia.annotations.Max;
import org.sourceprojects.lycia.annotations.Min;
import org.sourceprojects.lycia.annotations.NotEmpty;
import org.sourceprojects.lycia.annotations.NotNull;
import org.sourceprojects.lycia.annotations.Null;
import org.sourceprojects.lycia.annotations.Past;
import org.sourceprojects.lycia.annotations.Pattern;
import org.sourceprojects.lycia.annotations.Size;
import org.sourceprojects.lycia.exceptions.LyciaConfigurationException;
import org.sourceprojects.lycia.exceptions.LyciaInitializationException;
import org.sourceprojects.lycia.internal.AnnotationCombination;
import org.sourceprojects.lycia.internal.generator.MethodParameterUtils;
import org.w3c.dom.Element;

/**
 * 
 * @author noctarius
 * @since 1.9.1
 */
@SuppressWarnings("unchecked")
public class ConstraintValidatorUtils {
	private static final Map<Class<? extends Annotation>, Class<? extends ConstraintValidator<?>>[]> MAPPING = new HashMap<Class<? extends Annotation>, Class<? extends ConstraintValidator<?>>[]>();

	private static final AnnotationCombination[] INOPERATIVE_ANNOTATION_COMBINATIONS = {
			new AnnotationCombination(NotEmpty.class, Empty.class),
			new AnnotationCombination(NotNull.class, Null.class),
			new AnnotationCombination(AssertFalse.class, AssertTrue.class),
			new AnnotationCombination(Future.class, Past.class),
			new AnnotationCombination(Empty.class, NotNull.class),
			new AnnotationCombination(Null.class, NotEmpty.class),
			new AnnotationCombination(DecimalMin.class, Size.class),
			new AnnotationCombination(DecimalMax.class, Size.class),
			new AnnotationCombination(Min.class, Size.class),
			new AnnotationCombination(Max.class, Size.class), };

	private ConstraintValidatorUtils() {
	}

	static {
		MAPPING.put(AssertFalse.class,
				new Class[] { AssertFalseConstraintValidator.class });
		MAPPING.put(AssertTrue.class,
				new Class[] { AssertTrueConstraintValidator.class });
		MAPPING.put(DecimalMax.class,
				new Class[] { DecimalMaxConstraintValidator.class });
		MAPPING.put(DecimalMin.class,
				new Class[] { DecimalMinConstraintValidator.class });
		MAPPING.put(Digits.class,
				new Class[] { DigitsConstraintValidator.class });
		MAPPING
				.put(Empty.class,
						new Class[] { EmptyConstraintValidator.class });
		MAPPING.put(Future.class,
				new Class[] { FutureConstraintValidator.class });
		MAPPING.put(Max.class, new Class[] { MaxConstraintValidator.class });
		MAPPING.put(Min.class, new Class[] { MinConstraintValidator.class });
		MAPPING.put(NotEmpty.class,
				new Class[] { NotEmptyConstraintValidator.class });
		MAPPING.put(NotNull.class,
				new Class[] { NotNullConstraintValidator.class });
		MAPPING.put(Null.class, new Class[] { NullConstraintValidator.class });
		MAPPING.put(Past.class, new Class[] { PastConstraintValidator.class });
		MAPPING.put(Pattern.class,
				new Class[] { PatternConstraintValidator.class });
		MAPPING.put(Size.class, new Class[] { SizeConstraintValidator.class });
	}

	public static <B> boolean checkConstraints(
			final PropertyDescriptor<B>[] properties, final Element element,
			final LyciaParser<B> lyciaParser) {

		for (final PropertyDescriptor<B> property : properties) {
			final ValidationField validationField = property
					.getValidationField();
			final Annotation[] annotations = getCompliantAnnotations(validationField);
			for (final Annotation annotation : annotations) {
				final ConstraintValidator<Annotation>[] validators = ConstraintValidatorUtils
						.findConstraintValidators(annotation);

				if (validators != null) {
					for (final ConstraintValidator<Annotation> validator : validators) {
						validator.initialize(annotation);

						final Object value = validationField.getValue(element,
								lyciaParser);

						if (!validator.isValid(value)) {
							return false;
						}
					}
				}
			}
		}

		return true;
	}

	public static final <B> PropertyDescriptor<B>[] buildPropertyDescriptors(
			final Method method, final LyciaParser<B> lyciaParser) {

		final List<PropertyDescriptor> result = new ArrayList<PropertyDescriptor>();

		final Class<?>[] parameterTypes = method.getParameterTypes();
		final TypeVariable<Method>[] typeVariables = method.getTypeParameters();
		final Annotation[][] annotations = method.getParameterAnnotations();

		for (int i = 0; i < parameterTypes.length; i++) {
			final Class<?> parameterType = parameterTypes[i];
			final String name = method.getName()
					+ "::"
					+ (typeVariables.length > i ? typeVariables[i].getName()
							: "parameter<" + i + ">");

			final Annotation[] reordered = MethodParameterUtils
					.precheckAndReorderAnnotations(method, i, annotations[i],
							lyciaParser);

			final ValidationField<B> validationField = new ParameterValidationField<B>(
					parameterType, reordered, method.getDeclaringClass(), name);

			if (getCompliantAnnotations(validationField).length > 0) {
				result.add(new PropertyDescriptor<B>(validationField));
			}
		}

		return result.toArray(new PropertyDescriptor[result.size()]);
	}

	public static final <B> PropertyDescriptor<B>[] buildProperDescriptors(
			final Class<?> validator) {

		final List<PropertyDescriptor<B>> result = new ArrayList<PropertyDescriptor<B>>();

		for (final Field field : validator.getDeclaredFields()) {
			final ValidationField<B> validationField = new FieldValidationField<B>(
					field);

			if (getCompliantAnnotations(validationField).length > 0) {
				result.add(new PropertyDescriptor<B>(validationField));
			}
		}

		return result.toArray(new PropertyDescriptor[result.size()]);
	}

	public static Annotation[] getCompliantAnnotations(
			final ValidationField field) {
		final List<Annotation> annotations = new ArrayList<Annotation>();

		for (final Annotation annotation : field.getAnnotations()) {
			if (annotation.annotationType().getAnnotation(Constraint.class) != null) {
				annotations.add(annotation);
			}
		}

		for (final AnnotationCombination combination : INOPERATIVE_ANNOTATION_COMBINATIONS) {
			boolean found = false;

			for (final Annotation annotation : annotations) {
				for (final Class<?> annotationType : combination
						.getAnnotations()) {
					if (annotation.annotationType().equals(annotationType)) {
						if (found) {
							final String classname = field.getDeclaringClass()
									.getCanonicalName();

							throw new LyciaInitializationException(
									"Inoperative annotation combination was found on Class '"
											+ classname + "::"
											+ field.getName() + "'");
						} else {
							found = true;
						}
					}
				}
			}
		}

		return annotations.toArray(new Annotation[annotations.size()]);
	}

	public static <A extends Annotation> ConstraintValidator<A>[] findConstraintValidators(
			final A annotation) {

		Class<? extends ConstraintValidator<?>>[] classes = findConstraintValidators0(annotation);

		if (classes == null || classes.length == 0) {
			classes = MAPPING.get(annotation.annotationType());
		}

		if (classes == null || classes.length == 0) {
			throw new LyciaConfigurationException(
					"No ConstraintValidator class for annotation "
							+ annotation.annotationType().getCanonicalName()
							+ " found");
		}

		final ConstraintValidator<A>[] validators = new ConstraintValidator[classes.length];
		for (int i = 0; i < classes.length; i++) {
			final Class<? extends ConstraintValidator<?>> clazz = classes[i];

			validators[i] = ConstraintValidatorFactory.newInstance(clazz);
		}

		return validators;
	}

	private static <A extends Annotation> Class<? extends ConstraintValidator<?>>[] findConstraintValidators0(
			final A annotation) {

		final Constraint constraint = annotation.annotationType()
				.getAnnotation(Constraint.class);

		if (constraint != null) {
			return constraint.validatedBy();
		}

		return new Class[0];
	}

}
